using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Reflection;

namespace PDO.Utilities
{
    // Copied from - http://www.interact-sw.co.uk/iangblog/2004/03/23/locking
    //
    // Thanks to Eric Gunnerson for recommending this be a struct rather
    // than a class - avoids a heap allocation.
    // Thanks to Change Gillespie and Jocelyn Coulmance for pointing out
    // the bugs that then crept in when I changed it to use struct...
    // Thanks to John Sands for providing the necessary incentive to make
    // me invent a way of using a struct in both release and debug builds
    // without losing the debug leak tracking.


    /// <summary>
    /// Provides a locking structure that allows determinate timeouts to be specified.
    /// </summary>
    public struct TimedLock : IDisposable
    {
        private static int LockTimeoutInterval; // seconds

        public static TimedLock Lock(object o)
        {
#if DEBUG
            // Set to small value to identify performance hot spots. 
            // Set to large value to allow time for investigating when on a breakpoint, etc.
            LockTimeoutInterval = 600; 
#else
            LockTimeoutInterval = 60; // seconds
#endif
            return Lock(o, LockTimeoutInterval);
        }

        public static TimedLock Lock(object o, int timeout)
        {
            LockTimeoutInterval = timeout;
            TimedLock tl = new TimedLock(o);

            if (!Monitor.TryEnter(o, TimeSpan.FromSeconds(LockTimeoutInterval)))
            {
#if DEBUG
                System.GC.SuppressFinalize(tl._leakDetector);

                Debug.WriteLine(Environment.NewLine + "Lock timeout of " + LockTimeoutInterval + " (seconds) expired at " + DateTime.Now + ".");
                Debug.WriteLine(">>> Last successfully acquired Lock callstack");
                Debug.Write(_acquiredLock[o]);
                Debug.WriteLine("<<< End successfully Locked stack dump." + Environment.NewLine);

                Debug.WriteLine(">*>*> Timed-out waiting callstack");
                Debug.Write(GetCallStack());
                Debug.WriteLine("<*<*< End timed-out waiting stack dump." + Environment.NewLine);
#endif
                throw new LockTimeoutException();
            }

#if DEBUG
            // Maintain dictionary of call stacks organized by lock object.
            if (_acquiredLock.ContainsKey(o))
            {
                _acquiredLock[o] = GetCallStack();
            }
            else
            {
                _acquiredLock.Add(o, GetCallStack());
            }
#endif
            return tl;
        }

        private TimedLock(object o)
        {
            _target = o;
#if DEBUG
            _leakDetector = new Sentinel();
#endif
        }
        private object _target;

        public void Dispose()
        {
            
            Monitor.Exit(_target);

            // It's a bad error if someone forgets to call Dispose,
            // so in Debug builds, we put a finalizer in to detect
            // the error. If Dispose is called, we suppress the
            // finalizer.
#if DEBUG
            GC.SuppressFinalize(_leakDetector);
#endif
        }

#if DEBUG
        // (In Debug mode, we make it a class so that we can add a finalizer
        // in order to detect when the object is not freed.)
        private class Sentinel
        {
            ~Sentinel()
            {
                // If this finalizer runs, someone somewhere failed to
                // call Dispose, which means we've failed to leave
                // a monitor!
                System.Diagnostics.Debug.Fail("Undisposed lock");
            }
        }
        private Sentinel _leakDetector;


        /// <summary>
        /// Collect call stack and other calling information.
        /// </summary>
        private static string GetCallStack()
        {
            StackTrace st = new StackTrace(true);
            StringBuilder sb = new StringBuilder();

            Assembly a = Assembly.GetEntryAssembly();
            if (a != null)
            {
                sb.Append("Process Name:" + a.FullName + "\r\n");
            }
            else
            {
                sb.Append("Executing Assembly:" + Assembly.GetExecutingAssembly().FullName + "\r\n");
            }

            sb.Append("Time:" + DateTime.Now + "\r\n");
            for (int i = 0; i < st.FrameCount; i++)
            {
                StackFrame sf = st.GetFrame(i);
                string s = string.Format("{0} | {1} Line {2}, Column {3}{4}",
                    sf.GetFileName(), sf.GetMethod(), sf.GetFileLineNumber(), sf.GetFileColumnNumber(), Environment.NewLine);
                sb.Append(s);
            }
            return sb.ToString();
        }

        private static Dictionary<object, string> _acquiredLock = new Dictionary<object, string>();

#endif


    }
    public class LockTimeoutException : ApplicationException
    {
        public LockTimeoutException()
            : base("Timeout waiting for lock")
        {
        }
    }
}
